package io.gitee.zhangbinhub.acp.cloud.resource.server

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.dataformat.xml.XmlMapper
import io.gitee.zhangbinhub.acp.boot.AcpExceptionAutoConfiguration
import io.gitee.zhangbinhub.acp.cloud.AcpCloudComponentAutoConfiguration
import io.gitee.zhangbinhub.acp.cloud.AcpCloudPropertiesAutoConfiguration
import io.gitee.zhangbinhub.acp.cloud.constant.RestPrefix
import io.gitee.zhangbinhub.acp.cloud.resource.server.conf.AcpCloudResourceServerConfiguration
import io.gitee.zhangbinhub.acp.cloud.resource.server.error.AcpWebSecurityErrorHandler
import io.gitee.zhangbinhub.acp.core.common.CommonTools
import io.netty.channel.ChannelOption
import io.netty.handler.timeout.ReadTimeoutHandler
import org.springframework.aot.hint.annotation.RegisterReflectionForBinding
import org.springframework.boot.autoconfigure.AutoConfiguration
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication
import org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration
import org.springframework.boot.autoconfigure.web.ServerProperties
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder
import org.springframework.cloud.client.loadbalancer.LoadBalanced
import org.springframework.cloud.openfeign.support.FeignHttpClientProperties
import org.springframework.context.annotation.Bean
import org.springframework.http.MediaType
import org.springframework.http.client.reactive.ReactorClientHttpConnector
import org.springframework.http.client.support.BasicAuthenticationInterceptor
import org.springframework.http.codec.json.Jackson2JsonDecoder
import org.springframework.http.codec.json.Jackson2JsonEncoder
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter
import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver
import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver
import org.springframework.web.client.RestClient
import org.springframework.web.reactive.function.client.WebClient
import reactor.netty.http.client.HttpClient
import java.time.Duration

/**
 * @since JDK 17
 */
@AutoConfiguration(
    before = [OAuth2ResourceServerAutoConfiguration::class],
    after = [AcpCloudPropertiesAutoConfiguration::class, AcpCloudComponentAutoConfiguration::class, AcpExceptionAutoConfiguration::class]
)
@EnableConfigurationProperties(AcpCloudResourceServerConfiguration::class)
class AcpCloudResourceServerComponentAutoConfiguration(
    private val acpCloudResourceServerConfiguration: AcpCloudResourceServerConfiguration,
    serverProperties: ServerProperties
) {
    private val contextPath: String =
        if (CommonTools.isNullStr(serverProperties.servlet.contextPath)) "" else serverProperties.servlet.contextPath

    @Bean
    fun webSecurityErrorHandler() = AcpWebSecurityErrorHandler()

    @LoadBalanced
    @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
    @RegisterReflectionForBinding(classes = [RestClient.Builder::class])
    @Bean("acpSpringCloudResourceServerRestClientBuilder")
    fun acpSpringCloudResourceServerRestClientBuilder(
        feignHttpClientProperties: FeignHttpClientProperties,
        objectMapper: ObjectMapper,
        xmlMapper: XmlMapper
    ): RestClient.Builder = RestClient.builder()
        .requestFactory(
            ClientHttpRequestFactoryBuilder.httpComponents().withDefaultRequestConfigCustomizer {
                it.setConnectionRequestTimeout(
                    feignHttpClientProperties.hc5.connectionRequestTimeout.toLong(),
                    feignHttpClientProperties.hc5.connectionRequestTimeoutUnit
                )
                it.setResponseTimeout(
                    feignHttpClientProperties.hc5.socketTimeout.toLong(),
                    feignHttpClientProperties.hc5.socketTimeoutUnit
                )
            }.build()
        )
        .messageConverters { messageConverters ->
            messageConverters.add(MappingJackson2HttpMessageConverter(objectMapper))
            messageConverters.add(MappingJackson2XmlHttpMessageConverter(xmlMapper))
        }.requestInterceptors { requestInterceptors ->
            requestInterceptors.add(
                BasicAuthenticationInterceptor(
                    acpCloudResourceServerConfiguration.clientId, acpCloudResourceServerConfiguration.clientSecret
                )
            )
        }

    @LoadBalanced
    @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
    @RegisterReflectionForBinding(classes = [WebClient.Builder::class])
    @Bean("acpSpringCloudResourceServerWebClientBuilder")
    fun acpSpringCloudResourceServerWebClientBuilder(
        feignHttpClientProperties: FeignHttpClientProperties, objectMapper: ObjectMapper
    ): WebClient.Builder = WebClient.builder().clientConnector(
        ReactorClientHttpConnector(
            HttpClient.create()
                .option(
                    ChannelOption.CONNECT_TIMEOUT_MILLIS,
                    Duration.of(
                        feignHttpClientProperties.hc5.connectionRequestTimeout.toLong(),
                        feignHttpClientProperties.hc5.connectionRequestTimeoutUnit.toChronoUnit()
                    ).toMillis().toInt()
                )
                .doOnConnected {
                    it.addHandlerLast(
                        ReadTimeoutHandler(
                            feignHttpClientProperties.hc5.socketTimeout.toLong(),
                            feignHttpClientProperties.hc5.socketTimeoutUnit
                        )
                    )
                })
    ).codecs { configuration ->
        configuration.defaultCodecs().jackson2JsonEncoder(Jackson2JsonEncoder(objectMapper, MediaType.APPLICATION_JSON))
        configuration.defaultCodecs().jackson2JsonDecoder(Jackson2JsonDecoder(objectMapper, MediaType.APPLICATION_JSON))
    }.defaultHeaders { headers ->
        headers.setBasicAuth(
            acpCloudResourceServerConfiguration.clientId, acpCloudResourceServerConfiguration.clientSecret
        )
    }

    @Bean
    @ConditionalOnMissingBean(BearerTokenResolver::class)
    fun bearerTokenResolver(): BearerTokenResolver = DefaultBearerTokenResolver().apply {
        this.setAllowFormEncodedBodyParameter(true)
        this.setAllowUriQueryParameter(true)
    }

    /**
     * 放行安全检查的开放Path
     */
    fun permitAllPath() = mutableListOf<String>().apply {
        this.add("$contextPath/error")
        this.add("$contextPath/favicon.ico")
        this.add("$contextPath/swagger/**")
        this.add("$contextPath/swagger-resources")
        this.add("$contextPath/v3/api-docs/**")
        this.add("$contextPath/doc.html")
        this.add("$contextPath/webjars/**")
        acpCloudResourceServerConfiguration.permitAllPath.forEach { path -> this.add(contextPath + path) }
        this.add(contextPath + RestPrefix.OPEN + "/**")
    }
}
